//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------

namespace System.Activities.Runtime
{
    using System;
    using System.Activities.DynamicUpdate;
    using System.Collections.ObjectModel;
    using System.Runtime;
    using System.Runtime.Serialization;
    using System.Collections.Generic;

    [DataContract]
    internal sealed class LocationEnvironment : ActivityInstanceMap.IActivityReferenceWithEnvironment
    {
        static DummyLocation dummyLocation = new DummyLocation();

        bool isDisposed;
        bool hasHandles;
        ActivityExecutor executor;

        // These two fields should be null unless we're in between calls to Update() and OnDeserialized().
        // Therefore they should never need to serialize.
        IList<Location> locationsToUnregister;
        IList<LocationReference> locationsToRegister;

        Location[] locations;
        bool hasMappableLocations;
        LocationEnvironment parent;

        Location singleLocation;

        // This list keeps track of handles that are created and initialized.
        List<Handle> handles;

        // We store refCount - 1 because it is more likely to
        // be zero and skipped by serialization
        int referenceCountMinusOne;

        bool hasOwnerCompleted;
                
        // this ctor overload is to be exclusively used by DU
        // for creating a LocationEnvironment for "noSymbols" ActivityInstance
        internal LocationEnvironment(LocationEnvironment parent, int capacity) 
            : this(null, null, parent, capacity)
        {
        }
       
        internal LocationEnvironment(ActivityExecutor executor, Activity definition)
        {
            this.executor = executor;
            this.Definition = definition;
        }

        internal LocationEnvironment(ActivityExecutor executor, Activity definition, LocationEnvironment parent, int capacity)
            : this(executor, definition)
        {
            this.parent = parent;

            Fx.Assert(capacity > 0, "must have a positive capacity if using this overload");
            if (capacity > 1)
            {
                this.locations = new Location[capacity];
            }
        }

        [DataMember(EmitDefaultValue = false, Name = "locations")]
        internal Location[] SerializedLocations
        {
            get { return this.locations; }
            set { this.locations = value; }
        }

        [DataMember(EmitDefaultValue = false, Name = "hasMappableLocations")]
        internal bool SerializedHasMappableLocations
        {
            get { return this.hasMappableLocations; }
            set { this.hasMappableLocations = value; }
        }

        [DataMember(EmitDefaultValue = false, Name = "parent")]
        internal LocationEnvironment SerializedParent
        {
            get { return this.parent; }
            set { this.parent = value; }
        }

        [DataMember(EmitDefaultValue = false, Name = "singleLocation")]
        internal Location SerializedSingleLocation
        {
            get { return this.singleLocation; }
            set { this.singleLocation = value; }
        }

        [DataMember(EmitDefaultValue = false, Name = "handles")]
        internal List<Handle> SerializedHandles
        {
            get { return this.handles; }
            set { this.handles = value; }
        }

        [DataMember(EmitDefaultValue = false, Name = "referenceCountMinusOne")]
        internal int SerializedReferenceCountMinusOne
        {
            get { return this.referenceCountMinusOne; }
            set { this.referenceCountMinusOne = value; }
        }

        [DataMember(EmitDefaultValue = false, Name = "hasOwnerCompleted")]
        internal bool SerializedHasOwnerCompleted
        {
            get { return this.hasOwnerCompleted; }
            set { this.hasOwnerCompleted = value; }
        }

        internal Activity Definition
        {
            get;
            private set;
        }

        internal LocationEnvironment Parent
        {
            get
            {
                return this.parent;
            }
            set
            {
                this.parent = value;
            }
        }

        internal bool HasHandles
        {
            get
            {
                return this.hasHandles;
            }
        }

        MappableObjectManager MappableObjectManager
        {
            get
            {
                return this.executor.MappableObjectManager;
            }
        }

        internal bool ShouldDispose
        {
            get
            {
                return this.referenceCountMinusOne == -1;
            }
        }

        internal bool HasOwnerCompleted
        {
            get
            {
                return this.hasOwnerCompleted;
            }
        }

        Activity ActivityInstanceMap.IActivityReference.Activity
        {
            get
            {
                return this.Definition;
            }
        }

        internal List<Handle> Handles
        {
            get { return this.handles; }
        }

        void ActivityInstanceMap.IActivityReference.Load(Activity activity, ActivityInstanceMap instanceMap)
        {
            this.Definition = activity;
        }
        
        void ActivityInstanceMap.IActivityReferenceWithEnvironment.UpdateEnvironment(EnvironmentUpdateMap map, Activity activity)
        {
            // LocationEnvironment.Update() is invoked through this path when this is a seondary root's environment(and in its parent chain) whose owner has already completed.
            this.Update(map, activity);
        }        

        // Note that the owner should never call this as the first
        // AddReference is assumed
        internal void AddReference()
        {
            this.referenceCountMinusOne++;
        }

        internal void RemoveReference(bool isOwner)
        {
            if (isOwner)
            {
                this.hasOwnerCompleted = true;
            }

            Fx.Assert(this.referenceCountMinusOne >= 0, "We must at least have 1 reference (0 for refCountMinusOne)");
            this.referenceCountMinusOne--;
        }

        internal void OnDeserialized(ActivityExecutor executor, ActivityInstance handleScope)
        {
            this.executor = executor;

            // The instance map Load might have already set the definition to the correct one.
            // If not then we assume the definition is the same as the handle scope.
            if (this.Definition == null)
            {
                this.Definition = handleScope.Activity;
            }

            ReinitializeHandles(handleScope);
            RegisterUpdatedLocations(handleScope);
        }

        internal void ReinitializeHandles(ActivityInstance handleScope)
        {
            // Need to reinitialize the handles in the list.
            if (this.handles != null)
            {
                int count = this.handles.Count;
                for (int i = 0; i < count; i++)
                {
                    this.handles[i].Reinitialize(handleScope);
                    this.hasHandles = true;
                }
            }
        }

        internal void Dispose()
        {
            Fx.Assert(this.ShouldDispose, "We shouldn't be calling Dispose when we have existing references.");
            Fx.Assert(!this.hasHandles, "We should have already uninitialized the handles and set our hasHandles variable to false.");
            Fx.Assert(!this.isDisposed, "We should not already be disposed.");

            this.isDisposed = true;

            CleanupMappedLocations();
        }

        internal void AddHandle(Handle handleToAdd)
        {
            if (this.handles == null)
            {
                this.handles = new List<Handle>();
            }
            this.handles.Add(handleToAdd);
            this.hasHandles = true;
        }

        void CleanupMappedLocations()
        {
            if (this.hasMappableLocations)
            {
                if (this.singleLocation != null)
                {
                    Fx.Assert(this.singleLocation.CanBeMapped, "Can only have mappable locations for a singleton if its mappable.");
                    UnregisterLocation(this.singleLocation);
                }
                else if (this.locations != null)
                {
                    for (int i = 0; i < this.locations.Length; i++)
                    {
                        Location location = this.locations[i];

                        if (location.CanBeMapped)
                        {
                            UnregisterLocation(location);
                        }
                    }
                }
            }
        }

        internal void UninitializeHandles(ActivityInstance scope)
        {
            if (this.hasHandles)
            {
                HandleInitializationContext context = null;

                try
                {
                    UninitializeHandles(scope, this.Definition.RuntimeVariables, ref context);
                    UninitializeHandles(scope, this.Definition.ImplementationVariables, ref context);

                    this.hasHandles = false;
                }
                finally
                {
                    if (context != null)
                    {
                        context.Dispose();
                    }
                }
            }
        }

        void UninitializeHandles(ActivityInstance scope, IList<Variable> variables, ref HandleInitializationContext context)
        {
            for (int i = 0; i < variables.Count; i++)
            {
                Variable variable = variables[i];
                Fx.Assert(variable.Owner == this.Definition, "We should only be targeting the vairables at this scope.");

                if (variable.IsHandle)
                {
                    Location location = GetSpecificLocation(variable.Id);

                    if (location != null)
                    {
                        Handle handle = (Handle)location.Value;

                        if (handle != null)
                        {
                            if (context == null)
                            {
                                context = new HandleInitializationContext(this.executor, scope);
                            }

                            handle.Uninitialize(context);
                        }

                        location.Value = null;
                    }
                }
            }
        }

        internal void DeclareHandle(LocationReference locationReference, Location location, ActivityInstance activityInstance)
        {
            this.hasHandles = true;

            Declare(locationReference, location, activityInstance);
        }

        internal void DeclareTemporaryLocation<T>(LocationReference locationReference, ActivityInstance activityInstance, bool bufferGetsOnCollapse)
            where T : Location
        {
            Location locationToDeclare = new Location<T>();
            locationToDeclare.SetTemporaryResolutionData(this, bufferGetsOnCollapse);

            this.Declare(locationReference, locationToDeclare, activityInstance);
        }

        internal void Declare(LocationReference locationReference, Location location, ActivityInstance activityInstance)
        {
            Fx.Assert((locationReference.Id == 0 && this.locations == null) || (locationReference.Id >= 0 && this.locations != null && locationReference.Id < this.locations.Length), "The environment should have been created with the appropriate capacity.");
            Fx.Assert(location != null, "");

            RegisterLocation(location, locationReference, activityInstance);

            if (this.locations == null)
            {
                Fx.Assert(this.singleLocation == null, "We should not have had a single location if we are trying to declare one.");
                Fx.Assert(locationReference.Id == 0, "We should think the id is zero if we are setting the single location.");

                this.singleLocation = location;
            }
            else
            {
                Fx.Assert(this.locations[locationReference.Id] == null || this.locations[locationReference.Id] is DummyLocation, "We should not have had a location at the spot we are replacing.");

                this.locations[locationReference.Id] = location;
            }
        }

        internal Location<T> GetSpecificLocation<T>(int id)
        {
            return GetSpecificLocation(id) as Location<T>;
        }

        internal Location GetSpecificLocation(int id)
        {
            Fx.Assert(id >= 0 && ((this.locations == null && id == 0) || (this.locations != null && id < this.locations.Length)), "Id needs to be within bounds.");

            if (this.locations == null)
            {
                return this.singleLocation;
            }
            else
            {
                return this.locations[id];
            }
        }

        // called for asynchronous argument resolution to collapse Location<Location<T>> to Location<T> in the environment
        internal void CollapseTemporaryResolutionLocations()
        {
            if (this.locations == null)
            {
                if (this.singleLocation != null &&
                    object.ReferenceEquals(this.singleLocation.TemporaryResolutionEnvironment, this))
                {
                    CollapseTemporaryResolutionLocation(ref this.singleLocation);
                }
            }
            else
            {
                for (int i = 0; i < this.locations.Length; i++)
                {
                    Location referenceLocation = this.locations[i];

                    if (referenceLocation != null &&
                        object.ReferenceEquals(referenceLocation.TemporaryResolutionEnvironment, this))
                    {
                        CollapseTemporaryResolutionLocation(ref this.locations[i]);
                    }
                }
            }
        }

        // Called after an argument is added in Dynamic Update, when we need to collapse
        // just one location rather than the whole environment
        internal void CollapseTemporaryResolutionLocation(Location location)
        {
            // This assert doesn't necessarily imply that the location is still part of this environment;
            // it might have been removed in a subsequent update. If so, this method is a no-op.
            Fx.Assert(location.TemporaryResolutionEnvironment == this, "Trying to collapse from the wrong environment");

            if (this.singleLocation == location)
            {
                CollapseTemporaryResolutionLocation(ref this.singleLocation);
            }
            else if (this.locations != null)
            {
                for (int i = 0; i < this.locations.Length; i++)
                {
                    if (this.locations[i] == location)
                    {
                        CollapseTemporaryResolutionLocation(ref this.locations[i]);
                    }
                }
            }
        }

        // Gets the location at this scope.  The caller verifies that ref.owner == this.definition.
        internal bool TryGetLocation(int id, out Location value)
        {
            ThrowIfDisposed();

            value = null;

            if (this.locations == null)
            {
                if (id == 0)
                {
                    value = this.singleLocation;
                }
            }
            else
            {
                if (this.locations.Length > id)
                {
                    value = this.locations[id];
                }
            }

            return value != null;
        }

        internal bool TryGetLocation(int id, Activity environmentOwner, out Location value)
        {
            ThrowIfDisposed();

            LocationEnvironment targetEnvironment = this;

            while (targetEnvironment != null && targetEnvironment.Definition != environmentOwner)
            {
                targetEnvironment = targetEnvironment.Parent;
            }

            if (targetEnvironment == null)
            {
                value = null;
                return false;
            }

            value = null;

            if (id == 0 && targetEnvironment.locations == null)
            {
                value = targetEnvironment.singleLocation;
            }
            else if (targetEnvironment.locations != null && targetEnvironment.locations.Length > id)
            {
                value = targetEnvironment.locations[id];
            }

            return value != null;
        }

        void RegisterLocation(Location location, LocationReference locationReference, ActivityInstance activityInstance)
        {
            if (location.CanBeMapped)
            {
                this.hasMappableLocations = true;
                this.MappableObjectManager.Register(location, this.Definition, locationReference, activityInstance);
            }
        }

        void UnregisterLocation(Location location)
        {
            this.MappableObjectManager.Unregister(location);
        }

        void ThrowIfDisposed()
        {
            if (isDisposed)
            {
                throw FxTrace.Exception.AsError(
                    new ObjectDisposedException(this.GetType().FullName, SR.EnvironmentDisposed));
            }
        }

        internal void Update(EnvironmentUpdateMap map, Activity activity)
        {
            //                    arguments     public variables      private variables    RuntimeDelegateArguments
            //  Locations array:  AAAAAAAAAA   VVVVVVVVVVVVVVVVVVVVVV PPPPPPPPPPPPPPPPPPP  DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD

            int actualRuntimeDelegateArgumentCount = activity.HandlerOf == null ? 0 : activity.HandlerOf.RuntimeDelegateArguments.Count;

            if (map.NewArgumentCount != activity.RuntimeArguments.Count ||
                map.NewVariableCount != activity.RuntimeVariables.Count ||
                map.NewPrivateVariableCount != activity.ImplementationVariables.Count ||
                map.RuntimeDelegateArgumentCount != actualRuntimeDelegateArgumentCount)
            {
                throw FxTrace.Exception.AsError(new InstanceUpdateException(SR.InvalidUpdateMap(
                    SR.WrongEnvironmentCount(activity, map.NewArgumentCount, map.NewVariableCount, map.NewPrivateVariableCount, map.RuntimeDelegateArgumentCount,
                        activity.RuntimeArguments.Count, activity.RuntimeVariables.Count, activity.ImplementationVariables.Count, actualRuntimeDelegateArgumentCount))));
            }

            int expectedLocationCount = map.OldArgumentCount + map.OldVariableCount + map.OldPrivateVariableCount + map.RuntimeDelegateArgumentCount;

            int actualLocationCount;
            if (this.locations == null)
            {
                if (this.singleLocation == null)
                {
                    // we can hit this condition when the root activity instance has zero symbol.
                    actualLocationCount = 0;
                }
                else
                {
                    actualLocationCount = 1;

                    // temporarily normalize to locations array for the sake of environment update processing
                    this.locations = new Location[] { this.singleLocation };
                    this.singleLocation = null;
                }
            }
            else
            {
                Fx.Assert(this.singleLocation == null, "locations and singleLocations cannot be non-null at the same time.");
                actualLocationCount = this.locations.Length;
            }

            if (expectedLocationCount != actualLocationCount)
            {
                throw FxTrace.Exception.AsError(new InstanceUpdateException(SR.InvalidUpdateMap(
                    SR.WrongOriginalEnvironmentCount(activity, map.OldArgumentCount, map.OldVariableCount, map.OldPrivateVariableCount, map.RuntimeDelegateArgumentCount,
                        expectedLocationCount, actualLocationCount))));
            }

            Location[] newLocations = null;

            // If newTotalLocations == 0, update will leave us with an empty LocationEnvironment,
            // which is something the runtime would normally never create. This is harmless, but it
            // is a loosening of normal invariants.
            int newTotalLocations = map.NewArgumentCount + map.NewVariableCount + map.NewPrivateVariableCount + map.RuntimeDelegateArgumentCount;
            if (newTotalLocations > 0)
            {
                newLocations = new Location[newTotalLocations];
            }

            UpdateArguments(map, newLocations);
            UnregisterRemovedVariables(map);
            UpdatePublicVariables(map, newLocations, activity);
            UpdatePrivateVariables(map, newLocations, activity);
            CopyRuntimeDelegateArguments(map, newLocations);

            Location newSingleLocation = null;
            if (newTotalLocations == 1)
            {
                newSingleLocation = newLocations[0];
                newLocations = null;
            }

            this.singleLocation = newSingleLocation;
            this.locations = newLocations;
        }

        void UpdateArguments(EnvironmentUpdateMap map, Location[] newLocations)
        {
            if (map.HasArgumentEntries)
            {
                for (int i = 0; i < map.ArgumentEntries.Count; i++)
                {
                    EnvironmentUpdateMapEntry entry = map.ArgumentEntries[i];

                    Fx.Assert(entry.NewOffset >= 0 && entry.NewOffset < map.NewArgumentCount, "Argument offset is out of range");

                    if (entry.IsAddition)
                    {
                        // Location allocation will be performed later during ResolveDynamicallyAddedArguments().
                        // for now, simply assign a dummy location so we know not to copy over the old value.
                        newLocations[entry.NewOffset] = dummyLocation;
                    }
                    else
                    {
                        Fx.Assert(this.locations != null && this.singleLocation == null, "Caller should have copied singleLocation into locations array");

                        // rearrangement of existing arguments
                        // this entry here doesn't describe argument removal
                        newLocations[entry.NewOffset] = this.locations[entry.OldOffset];
                    }
                }
            }

            // copy over unchanged Locations, and null out DummyLocations
            for (int i = 0; i < map.NewArgumentCount; i++)
            {
                if (newLocations[i] == null)
                {
                    Fx.Assert(this.locations != null && this.locations.Length > i, "locations must be non-null and index i must be within the range of locations.");
                    newLocations[i] = this.locations[i];
                }
                else if (newLocations[i] == dummyLocation)
                {
                    newLocations[i] = null;
                }
            }
        }

        void UpdatePublicVariables(EnvironmentUpdateMap map, Location[] newLocations, Activity activity)
        {           
            UpdateVariables(
                map.NewArgumentCount, 
                map.OldArgumentCount, 
                map.NewVariableCount, 
                map.OldVariableCount, 
                map.VariableEntries, 
                activity.RuntimeVariables, 
                newLocations);
        }

        void UpdatePrivateVariables(EnvironmentUpdateMap map, Location[] newLocations, Activity activity)
        {
            UpdateVariables(
                map.NewArgumentCount + map.NewVariableCount, 
                map.OldArgumentCount + map.OldVariableCount, 
                map.NewPrivateVariableCount, 
                map.OldPrivateVariableCount, 
                map.PrivateVariableEntries, 
                activity.ImplementationVariables, 
                newLocations);
        }

        void UpdateVariables(int newVariablesOffset, int oldVariablesOffset, int newVariableCount, int oldVariableCount, IList<EnvironmentUpdateMapEntry> variableEntries, IList<Variable> variables, Location[] newLocations)
        {
            if (variableEntries != null)
            {
                for (int i = 0; i < variableEntries.Count; i++)
                {
                    EnvironmentUpdateMapEntry entry = variableEntries[i];

                    Fx.Assert(entry.NewOffset >= 0 && entry.NewOffset < newVariableCount, "Variable offset is out of range");
                    Fx.Assert(!entry.IsNewHandle, "This should have been caught in ActivityInstanceMap.UpdateRawInstance");

                    if (entry.IsAddition)
                    {
                        Variable newVariable = variables[entry.NewOffset];
                        Location location = newVariable.CreateLocation();
                        newLocations[newVariablesOffset + entry.NewOffset] = location;
                        if (location.CanBeMapped)
                        {
                            ActivityUtilities.Add(ref this.locationsToRegister, newVariable);
                        }
                    }
                    else
                    {
                        Fx.Assert(this.locations != null && this.singleLocation == null, "Caller should have copied singleLocation into locations array");

                        // rearrangement of existing variable
                        // this entry here doesn't describe variable removal
                        newLocations[newVariablesOffset + entry.NewOffset] = this.locations[oldVariablesOffset + entry.OldOffset];
                    }
                }
            }

            // copy over unchanged variable Locations
            for (int i = 0; i < newVariableCount; i++)
            {
                if (newLocations[newVariablesOffset + i] == null)
                {
                    Fx.Assert(i < oldVariableCount, "New variable should have a location");
                    Fx.Assert(this.locations != null && this.locations.Length > oldVariablesOffset + i, "locations must be non-null and index i + oldVariableOffset must be within the range of locations.");

                    newLocations[newVariablesOffset + i] = this.locations[oldVariablesOffset + i];
                }
            }
        }

        void CopyRuntimeDelegateArguments(EnvironmentUpdateMap map, Location[] newLocations)
        {
            for (int i = 1; i <= map.RuntimeDelegateArgumentCount; i++)
            {
                newLocations[newLocations.Length - i] = this.locations[this.locations.Length - i];
            }
        }

        void CollapseTemporaryResolutionLocation(ref Location location)
        {
            if (location.Value == null)
            {
                location = (Location)location.CreateDefaultValue();
            }
            else
            {
                location = ((Location)location.Value).CreateReference(location.BufferGetsOnCollapse);
            }
        }

        void RegisterUpdatedLocations(ActivityInstance activityInstance)
        {
            if (this.locationsToRegister != null)
            {
                foreach (LocationReference locationReference in this.locationsToRegister)
                {
                    RegisterLocation(GetSpecificLocation(locationReference.Id), locationReference, activityInstance);
                }
                this.locationsToRegister = null;
            }

            if (this.locationsToUnregister != null)
            {
                foreach (Location location in this.locationsToUnregister)
                {
                    UnregisterLocation(location);
                }
                this.locationsToUnregister = null;
            }
        }

        void UnregisterRemovedVariables(EnvironmentUpdateMap map)
        {
            bool hasMappableLocationsRemaining = false;
            int offset = map.OldArgumentCount;

            FindVariablesToUnregister(false, map, map.OldVariableCount, offset, ref hasMappableLocationsRemaining);
            
            offset = map.OldArgumentCount + map.OldVariableCount;

            FindVariablesToUnregister(true, map, map.OldPrivateVariableCount, offset, ref hasMappableLocationsRemaining);

            this.hasMappableLocations = hasMappableLocationsRemaining;
        }

        delegate int? GetNewVariableIndex(int oldIndex);
        private void FindVariablesToUnregister(bool forImplementation, EnvironmentUpdateMap map, int oldVariableCount, int offset, ref bool hasMappableLocationsRemaining)
        {
            for (int i = 0; i < oldVariableCount; i++)
            {
                Location location = this.locations[i + offset];
                if (location.CanBeMapped)
                {
                    if ((forImplementation && map.GetNewPrivateVariableIndex(i).HasValue) || (!forImplementation && map.GetNewVariableIndex(i).HasValue))
                    {
                        hasMappableLocationsRemaining = true;
                    }
                    else
                    {
                        ActivityUtilities.Add(ref this.locationsToUnregister, location);
                    }
                }
            }
        }

        private class DummyLocation : Location<object>
        {
            // this is a dummy location 
            // temporarary place holder for a dynamically added LocationReference
        }
    }
}
